﻿/**
 * Licensed under the MIT License
 *
 * Copyright (c) 2010 Alexandre Croiseaux
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 */
package marcel.media.sound
{
	import flash.media.Sound;
	import flash.media.SoundChannel;
	import flash.media.SoundTransform;
	import flash.utils.Dictionary;
	import marcel.display.Tween;
	
	/**
	 * SoundManager is a Sound Management and playback utility for manipulating and working with internal sounds.
	 * @author Alexandre Croiseaux
	 */
	public class SoundManager
	{
		//--------------------------------------------------------------------------
		//
		//  Private vars
		//
		//--------------------------------------------------------------------------
		private var _sounds:Dictionary;
		private var _streams:Dictionary;
		private var _channels:Dictionary;
		private var _volumes:Dictionary;
		private var _volumeMultiplier:Number;
		
		
		//--------------------------------------------------------------------------
		//
		//  Constructor
		//
		//--------------------------------------------------------------------------
		/**
		 * Constructs a new SoundManager instance and sets the volume multiplier to 1
		 */
		public function SoundManager()
		{
			_sounds = new Dictionary();
			_streams = new Dictionary();
			_channels = new Dictionary();
			_volumes = new Dictionary();
			_volumeMultiplier = 1;
		}
		
		
		//--------------------------------------------------------------------------
		//
		//  Public methods
		//
		//--------------------------------------------------------------------------
		/**
		 * Add a new Sound inatnce into the manager, identified by an String id
		 * @param	id	a string identifying the sound
		 * @param	snd	a Sound instance
		 * @return	true if the sound has been added successfully, false if a sound with the same id already exists in this manager
		 */
		public function addSound(id:String, snd:Sound):Boolean
		{
			if (containsSound(id)) return false;
			_sounds[id] = snd;
			_volumes[id] = 1;
			return true;
		}
		
		public function addSoundStream(id:String, sndStream:SoundStream):Boolean
		{
			var added:Boolean = addSound(id, sndStream.getSound());
			if (added) _streams[id] = sndStream;
			return added;
		}
		
		/**
		 * Remove the sound instance identified by the id
		 * @param	id	sound id to remove
		 * @return	true if the sound has been removed successfully, false if no sound with this id has been found
		 */
		public function removeSound(id:String):Boolean
		{
			if (!containsSound(id)) return false;
			_sounds[id] = null;
			_channels[id] = null;
			_volumes[id] = null;
			_streams[id] = null;
			return true;
		}
		
		/**
		 * Indicates if the current manager contains the specified sound
		 * @param	id	sound id to remove.
		 * @return	true if the manager contains the sound, false otherwise.
		 */
		public function containsSound(id:String):Boolean
		{
			return _sounds[id] != null;
		}
		
		/**
		 * Returns the Sound instance identified by the id
		 * @param	id	a string identifying the sound
		 * @return	a Sound instance
		 */
		public function getSound(id:String):Sound
		{
			return _sounds[id];
		}
		
		/**
		 * Returns the SoundStream instance identified by the id
		 * @param	id	a string identifying the sound
		 * @return	a SoundStream instance
		 */
		public function getSoundStream(id:String):SoundStream
		{
			return _streams[id];
		}
		
		/**
		 * Returns the SoundChannel instance from the sound identified by the id
		 * @param	id	a string identifying the sound
		 * @return	a SoundChannel instance
		 */
		public function getChannel(id:String):SoundChannel
		{
			if (_streams[id] != null) return SoundStream(_streams[id]).getSoundChannel();
			return _channels[id];
		}
		
		/**
		 * Sets the volume multiplier value, and multiplies all sounds volume by the passed value.
		 * @param	volume	the multiplier value
		 */
		public function set volumeMultiplier(volume:Number):void
		{
			_volumeMultiplier = volume;
			for (var id:String in _channels)
			{
				var channel:SoundChannel = getChannel(id);
				if (channel)
				{
					var currentVolume:Number = _volumes[id];
					var sd:SoundTransform = channel.soundTransform;
					sd.volume =  currentVolume * _volumeMultiplier;
					channel.soundTransform = sd;
					Tween.removeTweens(channel);
				}
			}
		}
		
		/**
		 * Returns the current volume multiplier value
		 * @return	the multiplier value
		 */
		public function get volumeMultiplier():Number
		{
			return _volumeMultiplier;
		}
		
		/**
		 * Starts playing the specified sound
		 * @param	id		a string identifying the sound
		 * @param	loops		number of sound loops
		 * @param	volume		the sound initial volume
		 * @param	fadeInDuration	optional volume fade in duration, in seconds, fade goes from 0 to 'volume'
		 * @param	fadeEasing	optional easing function for the fade
		 * @param	startOffset	The initial position in milliseconds at which playback should start.
		 */
		public function play(id:String, loops:uint = 1, volume:Number = 1, fadeInDuration:Number = 0, fadeEasing:Function = null, startOffset:Number = 0):void
		{
			try
			{
				_volumes[id] = volume;
				volume *= _volumeMultiplier;
				
				var snd:Sound = getSound(id);
				_channels[id] = getSound(id).play(startOffset, loops);
				var sd:SoundTransform = getChannel(id).soundTransform;
				sd.volume = volume;
				getChannel(id).soundTransform = sd;
				
				if (fadeInDuration > 0) fade(id, 0, volume, false, false, fadeInDuration, fadeEasing);
			}
			catch(e:Error) { throw new Error("[SoundManager] sound with id '" + id + "' not found ! "); }
		}
		
		/**
		 * Load and starts playing the specified SoundStream
		 * @param	id		a string identifying the sound
		 * @param	mp3URI		the URI of the MP3 file to play
		 * @param	loop		indicates if the sound must loop
		 * @param	volume		the sound initial volume
		 * @param	fadeInDuration	optional volume fade in duration, in seconds, fade goes from 0 to 'volume'
		 * @param	fadeEasing	optional easing function for the fade
		 * @param	mp3Bitrate 	the bitrate of the loaded MP3 file, used to calculate the buffer time with the 'rate' property.
		 */
		public function load(id:String, mp3URI:String, loop:Boolean = false, volume:Number = 1, fadeInDuration:Number = 0, fadeEasing:Function = null, mp3Bitrate:uint = 128000):SoundStream
		{
			try
			{
				_volumes[id] = volume;
				volume *= _volumeMultiplier;
				
				var ss:SoundStream = _streams[id];
				ss.loop = loop;
				_channels[id] = ss.load(mp3URI, mp3Bitrate);
				ss.volume = volume;
				
				if (fadeInDuration > 0) fade(id, 0, volume, false, false, fadeInDuration, fadeEasing);
				
				return ss;
			}
			catch (e:Error) { throw new Error("[SoundManager] SoundStream with id '" + id + "' not found !"); }
			
			return null;
		}
		
		/**
		 * Stops playing the specified sound
		 * @param	id		a string identifying the sound
		 * @param	fadeDuration	optional volume fade in duration, in seconds, fade goes from current volume to 0
		 * @param	fadeEasing	optional easing function for the fade
		 */
		public function stop(id:String, fadeDuration:Number = 0, fadeEasing:Function = null):void
		{
			var ch:SoundChannel = getChannel(id);
			if (ch)
			{
				if (fadeDuration > 0) fade(id, ch.soundTransform.volume, 0, false, true, fadeDuration, fadeEasing);
				else
				{
					var ss:SoundStream = getSoundStream(id);
					if (ss != null) ss.stop();
					else getChannel(id).stop();
				}
			}
		}
		
		/**
		 * Stops all playing sounds
		 * @param	fadeDuration	optional volume fade in duration, in seconds, fade goes from current volume to 0
		 * @param	fadeEasing	optional easing function for the fade
		 */
		public function stopAllSounds(fadeDuration:Number = 0, fadeEasing:Function = null):void
		{
			for (var id:String in _channels)
			{
				if (fadeDuration > 0) fade(id, getChannel(id).soundTransform.volume, 0, false, true, fadeDuration, fadeEasing);
				else stop(id);
			}
		}
		
		
		/**
		 * Fades the specified sound
		 * @param	id		a string identifying the sound
		 * @param	startVolume	the volume to start the fade from, range [0-1]
		 * @param	endVolume	the volume to end the fade to, range [0-1]
		 * @param	start		indicates if the sound must be started with the fade
		 * @param	stopAtEnd	indicates if the sound must be stopped at the end of the fade
		 * @param	duration	optional volume fade in duration, in seconds, fade goes from 'startVolume' volume to 'endVolume'
		 * @param	easing		optional easing function for the fade
		 */
		public function fade(id:String, startVolume:Number, endVolume:Number, start:Boolean = false, stopAtEnd:Boolean = false, duration:Number = 1, easing:Function = null):void
		{
			_volumes[id] = endVolume;
			startVolume *= _volumeMultiplier;
			endVolume *= _volumeMultiplier;
			
			if (start) play(id);
			
			var ch:SoundChannel = getChannel(id);
			if (ch)
			{
				var sd:SoundTransform = ch.soundTransform;
				sd.volume = startVolume;
				ch.soundTransform = sd;
				
				var extra:Object;
				if (stopAtEnd) extra = {onComplete: function():void { stop(id); }};
				// Tween.volumeTo(ch, endVolume, duration, easing, 0, extra);
			}
		}
	}
}